#!/usr/bin/python3

# This file is part of Cockpit.
#
# Copyright (C) 2020 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import parent

from testlib import *


@skipImage("No realmd available", "fedora-coreos")
@skipImage("freeipa not currently available", "debian-stable", "debian-testing", "ubuntu-2004", "arch")
@skipImage("Skip these for now, unsure what's broken", "ubuntu-stable")
@skipDistroPackage()
@skipImage("Needs sssd >=2.4.1", "rhel-8-4", "rhel-8-5")
class TestS4USsh(MachineCase):
    provision = {
        "0": {"address": "10.111.113.1/20", "dns": "10.111.112.100"},
        "1": {"address": "10.111.113.2/20", "dns": "10.111.112.100"},
        "services": {"image": "services", "memory_mb": 2048}
    }

    def setUp(self):
        super().setUp()
        self.machines['services'].execute("/root/run-freeipa")
        # during image creation the /var/cache directory gets cleaned up, recreate the krb5rcache
        self.machines['0'].execute("mkdir -pZ /var/cache/krb5rcache")
        self.machines['1'].execute("mkdir -pZ /var/cache/krb5rcache")

    def testBasic(self):
        client_machine = self.machine
        b = self.browser
        sshd_machine = self.machines["1"]
        ipa_machine = self.machines["services"]

        # set hostname for readable logs
        client_machine.execute("hostnamectl set-hostname sshclient.cockpit.lan")
        sshd_machine.execute("hostnamectl set-hostname sshserver.cockpit.lan")

        # join both machines
        wait(lambda: client_machine.execute("nslookup -type=SRV _ldap._tcp.cockpit.lan"))
        wait(lambda: sshd_machine.execute("nslookup -type=SRV _ldap._tcp.cockpit.lan"))
        sshd_machine.execute("echo foobarfoo | realm join -U admin cockpit.lan")
        # join client machine with Cockpit, to create the HTTP/ principal
        self.login_and_go("/system")
        b.click("#system_information_domain_button")
        b.wait_popup("realms-join-dialog")
        b.wait_attr("#realms-op-address", "data-discover", "done")
        b.set_input_text("#realms-op-address", "cockpit.lan")
        b.wait_text("#realms-op-address-helper", "Contacted domain")
        b.set_input_text("#realms-op-admin", "admin")
        b.set_input_text("#realms-op-admin-password", "foobarfoo")
        b.click(f"#realms-join-dialog button{self.primary_btn_class}")
        with b.wait_timeout(300):
            b.wait_not_present("#realms-join-dialog")
        b.logout()
        # the above installed a new certificate; pick this up, enable cert auth and TLS
        client_machine.write("/etc/cockpit/cockpit.conf", '[WebService]\nClientCertAuthentication = true\n', append=True)
        client_machine.start_cockpit(tls=True)

        # configure ipa to trust cockpit-generated S4U tickets for accessing our hosts
        with open("src/tls/ca/alice.pem") as f:
            alice_cert = f.read().strip()
        # IPA does not like the ---BEGIN/END lines
        alice_cert = '\n'.join([line for line in alice_cert.splitlines() if not line.startswith("----")])
        script = f"""set -eu
        echo foobarfoo | kinit admin@COCKPIT.LAN

        # add user to impersonate
        ipa user-add --first=user --last=user user
        echo foobarfoo | ipa user-mod user --password
        ipa user-add-cert user --certificate="{ alice_cert }"

        # set up delegation rule
        ipa servicedelegationtarget-add cockpit-target
        # FIXME: how to allow this to all machines in the network? https://pagure.io/freeipa/issue/8927
        ipa servicedelegationtarget-add-member cockpit-target --principals="host/sshserver.cockpit.lan@COCKPIT.LAN" --principals "host/sshclient.cockpit.lan@COCKPIT.LAN"
        ipa servicedelegationrule-add cockpit-delegation
        ipa servicedelegationrule-add-member cockpit-delegation --principals="HTTP/sshclient.cockpit.lan@COCKPIT.LAN"
        ipa servicedelegationrule-add-target cockpit-delegation --servicedelegationtargets="cockpit-target"
        """
        ipa_machine.execute(f"podman exec freeipa bash -ec '{script}'")

        # tell sssd about our CA for validating certs
        client_machine.execute("mkdir -p /etc/sssd/pki")
        with open("src/tls/ca/ca.pem") as f:
            client_machine.write("/etc/sssd/pki/sssd_auth_ca_db.pem", f.read())

        # enable gssapiauth for ssh
        sshd_machine.execute("sed -ri 's/#GSSAPIAuthentication (yes|no)/GSSAPIAuthentication yes/' /etc/ssh/sshd_config")
        sshd_machine.execute("systemctl restart sshd")

        # enable PAM kerberos authentication for sudo; see man pam_sss_gss(8)
        for m in [client_machine, sshd_machine]:
            m.execute(script=r"""#!/bin/sh -eu
            sed -i '/\[domain\/cockpit.lan\]/ a pam_gssapi_services = sudo, sudo-i' /etc/sssd/sssd.conf
            sed -i '1 a auth sufficient pam_sss_gss.so' /etc/pam.d/sudo
            systemctl restart sssd
            usermod -G wheel user
            """)

        # log into Cockpit with certificate, and grab the keytab
        client_machine.upload(["alice.pem", "alice.key"], "/var/tmp", relative_dir="src/tls/ca/")
        out = client_machine.execute(['curl', '-ksS', '-D-', '--cert', '/var/tmp/alice.pem', '--key', '/var/tmp/alice.key',
                                      'https://localhost:9090/cockpit/login'])
        self.assertIn("csrf-token", out)
        ccache_env = client_machine.execute("xargs -0n1 < /proc/$(pgrep cockpit-bridge)/environ | grep KRB5CCNAME=").strip()

        # SSH with the delegated ticket
        out = client_machine.execute(f"su -c 'KRB5_TRACE=/dev/stderr {ccache_env} ssh -vvv -K  user@sshserver.cockpit.lan echo hello' user")
        self.assertEqual(out.strip(), "hello")

        # direct sudo with the delegated ticket
        out = client_machine.execute(f'su -c "{ccache_env} sudo whoami" user')
        self.assertEqual(out.strip(), "root")

        # ssh -K is supposed to forward the credentials cache, but doesn't; klist in the ssh session is empty
        # and there is no ccache; so scp it manually; this emulates what cockpit-ssh will eventually do
        # (except that it would probably put the ticket into the default keyring)
        client_machine.execute(f"""su -c "KRB5_TRACE=/dev/stderr {ccache_env} scp {ccache_env.split('FILE:')[1]} user@sshserver.cockpit.lan:cache" user """)

        # ssh via s4u gssapi + sudo with the forwarded delegated ticket
        out = client_machine.execute(f"""su -c "KRB5_TRACE=/dev/stderr {ccache_env} ssh -vvv -K  user@sshserver.cockpit.lan 'KRB5CCNAME=/home/user/cache sudo whoami' " user""")
        self.assertEqual(out.strip(), "root")


if __name__ == '__main__':
    test_main()
